all repos — weimar @ 8d72190a752e3e1eaff992da40d75e01c6149993

Unnamed repository; edit this file 'description' to name the repository.

web/src/routes/image/[id]/+page.svelte (view raw)

  1<script lang="ts">
  2	import { onMount } from 'svelte';
  3	import { getImage, imageDownloadUrl, thumbnailUrl, type ImageDetail } from '$lib/api';
  4
  5	let { params } = $props();
  6	let id = $derived(parseInt(params.id));
  7
  8	let img = $state<ImageDetail | null>(null);
  9	let loading = $state(true);
 10	let error = $state('');
 11
 12	onMount(async () => {
 13		try {
 14			img = await getImage(id);
 15		} catch (err) {
 16			error = err instanceof Error ? err.message : 'Failed to load image';
 17		} finally {
 18			loading = false;
 19		}
 20	});
 21
 22	function formatSize(bytes: number): string {
 23		if (bytes < 1024) return bytes + ' B';
 24		if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
 25		return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
 26	}
 27
 28	function formatDate(s: string): string {
 29		return new Date(s).toLocaleString();
 30	}
 31</script>
 32
 33{#if loading}
 34	<p class="state-msg">Loading...</p>
 35{:else if error}
 36	<p class="state-msg error">{error}</p>
 37{:else if img}
 38	<div class="page-head">
 39		<h1>{img.filename}</h1>
 40		<div class="head-line"></div>
 41	</div>
 42
 43	<div class="layout">
 44		<div class="preview">
 45			{#if img.thumbnail_path}
 46				<img src={imageDownloadUrl(img.id)} alt={img.filename} />
 47			{:else}
 48				<div class="no-thumb">NO THUMBNAIL</div>
 49			{/if}
 50		</div>
 51
 52		<div class="meta">
 53			<div class="meta-block uploader-block">
 54				<span class="meta-label">UPLOADED BY</span>
 55				<div class="uploader-row">
 56					{#if img.avatar_url}
 57						<img src={img.avatar_url} alt="" class="avatar-img" />
 58					{/if}
 59					<span class="meta-value">{img.uploader_username}</span>
 60				</div>
 61			</div>
 62			<div class="meta-block">
 63				<span class="meta-label">ID</span>
 64				<span class="meta-value">#{img.id}</span>
 65			</div>
 66			<div class="meta-block">
 67				<span class="meta-label">DIMENSIONS</span>
 68				<span class="meta-value">{img.width} &times; {img.height}</span>
 69			</div>
 70			<div class="meta-block">
 71				<span class="meta-label">FILE SIZE</span>
 72				<span class="meta-value">{formatSize(img.file_size)}</span>
 73			</div>
 74			<div class="meta-block">
 75				<span class="meta-label">TYPE</span>
 76				<span class="meta-value">{img.mime_type}</span>
 77			</div>
 78			<div class="meta-block">
 79				<span class="meta-label">UPLOADED</span>
 80				<span class="meta-value">{formatDate(img.created_at)}</span>
 81			</div>
 82			<div class="meta-block tags-block">
 83				<span class="meta-label">TAGS</span>
 84				<div class="tag-pills">
 85					{#if img.tags_detail.length > 0}
 86						{#each img.tags_detail as t (t.tag)}
 87							<a href="/browse?tag={encodeURIComponent(t.tag)}" class="tag-pill" title="Used {t.usage_count} time{t.usage_count === 1 ? '' : 's'}">
 88								<span class="tag-name">{t.tag}</span>
 89								<span class="tag-count">{t.usage_count}</span>
 90							</a>
 91						{/each}
 92					{:else}
 93						<span class="none">none</span>
 94					{/if}
 95				</div>
 96			</div>
 97
 98			<a href={imageDownloadUrl(img.id)} class="btn" download={img.filename}>
 99				DOWNLOAD ORIGINAL
100			</a>
101		</div>
102	</div>
103
104	<p class="back"><a href="/browse">&larr; BACK TO BROWSE</a></p>
105{/if}
106
107<style>
108	.state-msg {
109		padding: 2rem 0;
110		font-size: 1.1rem;
111		color: #888;
112	}
113	.error {
114		color: #c62828;
115	}
116
117	.page-head {
118		margin-bottom: 1.5rem;
119	}
120	.page-head h1 {
121		word-break: break-all;
122	}
123	.head-line {
124		height: 3px;
125		background: #c62828;
126		width: 3rem;
127		margin-top: 0.4rem;
128	}
129
130	.layout {
131		display: flex;
132		gap: 2rem;
133		flex-wrap: wrap;
134	}
135	.preview {
136		flex: 1;
137		min-width: 300px;
138		background: #1a1a1a;
139		display: flex;
140		align-items: center;
141		justify-content: center;
142	}
143	.preview img {
144		display: block;
145		width: 100%;
146		height: auto;
147		max-height: 70vh;
148		object-fit: contain;
149	}
150	.no-thumb {
151		padding: 4rem 2rem;
152		color: #555;
153		font-family: 'Oswald', sans-serif;
154		font-weight: 500;
155		font-size: 0.9rem;
156		letter-spacing: 0.12em;
157	}
158
159	.meta {
160		width: 260px;
161		flex-shrink: 0;
162		display: flex;
163		flex-direction: column;
164		gap: 1rem;
165	}
166	.meta-block {
167		display: flex;
168		flex-direction: column;
169		gap: 0.15rem;
170	}
171	.meta-label {
172		font-family: 'Oswald', sans-serif;
173		font-weight: 500;
174		font-size: 0.7rem;
175		letter-spacing: 0.15em;
176		color: #999;
177	}
178	.meta-value {
179		font-family: 'Inter', sans-serif;
180		font-size: 0.9rem;
181		font-weight: 600;
182		color: #1a1a1a;
183	}
184
185	.uploader-row {
186		display: flex;
187		align-items: center;
188		gap: 0.4rem;
189	}
190	.avatar-img {
191		width: 1.5rem;
192		height: 1.5rem;
193		object-fit: cover;
194		flex-shrink: 0;
195	}
196
197	.tags-block {
198		flex: 1;
199	}
200	.tag-pills {
201		display: flex;
202		flex-wrap: wrap;
203		gap: 0.35rem;
204	}
205	.tag-pill {
206		display: inline-flex;
207		align-items: center;
208		gap: 0.3rem;
209		background: #1a1a1a;
210		padding: 0.25rem 0.5rem 0.25rem 0.65rem;
211		font-family: 'Inter', sans-serif;
212		font-size: 0.78rem;
213		font-weight: 500;
214		color: #eee;
215		text-decoration: none;
216		transition: background 0.15s;
217	}
218	.tag-pill:hover {
219		background: #c62828;
220		text-decoration: none;
221	}
222	.tag-name {
223		font-weight: 600;
224	}
225	.tag-count {
226		background: rgba(255, 255, 255, 0.12);
227		padding: 0.05rem 0.35rem;
228		font-size: 0.68rem;
229		font-weight: 600;
230		color: rgba(255, 255, 255, 0.65);
231		min-width: 1.1rem;
232		text-align: center;
233	}
234	.tag-pill:hover .tag-count {
235		background: rgba(0, 0, 0, 0.2);
236	}
237	.none {
238		color: #bbb;
239		font-size: 0.85rem;
240	}
241
242	.btn {
243		display: inline-flex;
244		align-items: center;
245		align-self: flex-start;
246		padding: 0.55rem 1.25rem;
247		background: #c62828;
248		color: #fff;
249		border: none;
250		font-family: 'Oswald', sans-serif;
251		font-weight: 500;
252		font-size: 0.8rem;
253		letter-spacing: 0.1em;
254		text-decoration: none;
255		text-transform: uppercase;
256		cursor: pointer;
257		transition: background 0.15s;
258		margin-top: 0.5rem;
259	}
260	.btn:hover {
261		background: #b71c1c;
262		text-decoration: none;
263	}
264
265	.back {
266		margin-top: 2rem;
267	}
268	.back a {
269		font-family: 'Oswald', sans-serif;
270		font-weight: 500;
271		font-size: 0.85rem;
272		letter-spacing: 0.1em;
273	}
274</style>